Newer
Older
client_dwatch / app / api / ds-devices / [serial_number] / logs / route.ts
/**
 * GET /ds-devices/{serial_number}/logs — 制御盤のログファイルの一覧(指示書AY 段階7-1。D-179)
 *
 * 仕様の正は `d-watch-api.yaml` の /ds-devices/{serial_number}/logs。
 * 一覧は毎回制御盤に問い合わせ(中継。最大15秒)、絞り込み・並べ替え・ページ分けは `queryDeviceLogs()` で行う(D8)。
 * ★サーバーには保存しない★。★検証の順序は シリアル(422)→ クエリ(422)→ 実在(404)→ 制御盤★。
 */
import { NextResponse } from "next/server";
import { parsePathSerialNumber } from "@/lib/api/path-params";
import { db } from "@/lib/db";
import { USE_FIXTURES } from "@/lib/db/use-fixtures";
import { errorBody, jsonError } from "@/lib/ds-agent/agent-request";
import { ensureFixtureAgent } from "@/lib/ds-agent/fixture-agent";
import { getRelay } from "@/lib/ds-agent/relay";
import { ensureDsAgentLogging, truncateAgentCode } from "@/lib/ds-agent/relay-log";
import { queryDeviceLogs } from "@/lib/ds-logs/query";
import { parseDeviceLogQuery } from "@/lib/ds-logs/validate";
import { logEvent, withRequestId } from "@/lib/logger";
import { logApiError } from "@/lib/logger/api";
import type { DsLogListResponse } from "@/lib/types";

const ROUTE = "/api/ds-devices/[serial_number]/logs";

export async function GET(
  request: Request,
  { params }: { params: Promise<{ serial_number: string }> }
) {
  return withRequestId(request, async () => {
    ensureDsAgentLogging();
    const { serial_number: serialRaw } = await params;
    const parsedSerial = parsePathSerialNumber(serialRaw);
    if (!parsedSerial.ok) return NextResponse.json(parsedSerial.error, { status: parsedSerial.status });
    const serial = parsedSerial.query;

    const parsedQuery = parseDeviceLogQuery(new URL(request.url).searchParams);
    if (!parsedQuery.ok) return NextResponse.json(parsedQuery.error, { status: parsedQuery.status });
    const query = parsedQuery.query;

    let exists: boolean;
    try {
      exists = await db.dsDeviceExists(serial);
    } catch (e) {
      const body = errorBody("internal_error", "データの取得に失敗しました");
      logApiError({ route: ROUTE, method: "GET", body, status: 500, err: e });
      return NextResponse.json(body, { status: 500 });
    }
    if (!exists) return jsonError(404, "not_found", "デバイスが見つかりません");

    const relay = getRelay();
    if (USE_FIXTURES) ensureFixtureAgent(relay, serial);

    const started = Date.now();
    const result = await relay.requestList(serial);
    const duration_ms = Date.now() - started;
    const logList = (
      r: "ok" | "offline" | "timeout" | "busy" | "agent_error",
      extra: { item_count?: number; truncated?: boolean; agent_code?: string } = {}
    ) =>
      logEvent("info", {
        event: "ds_logs.list",
        serial_number: serial,
        item_count: extra.item_count ?? 0,
        truncated: extra.truncated ?? false,
        duration_ms,
        result: r,
        ...(extra.agent_code !== undefined ? { agent_code: truncateAgentCode(extra.agent_code) } : {}),
      });

    switch (result.kind) {
      case "offline": {
        logList("offline");
        const lastSeen = relay.agentStatus(serial).last_seen_at;
        return jsonError(409, "agent_offline", "制御盤が接続されていません", {
          last_seen_at: lastSeen ? [lastSeen] : [],
        });
      }
      case "timeout":
        logList("timeout");
        return jsonError(504, "agent_timeout", "制御盤から応答がありません");
      case "busy":
        logList("busy");
        return jsonError(429, "agent_busy", "制御盤が混み合っています");
      case "agent_error":
        logList("agent_error", { agent_code: result.code });
        return jsonError(502, "agent_error", "制御盤でエラーが発生しました", { agent_code: [result.code] });
      case "ok": {
        const page = queryDeviceLogs(result.result.types, result.result.items, query);
        logList("ok", { item_count: result.result.items.length, truncated: result.result.truncated });
        const body: DsLogListResponse = {
          agent: relay.agentStatus(serial),
          types: result.result.types,
          items: page.items,
          truncated: result.result.truncated,
          meta: { total: page.total, page: query.page, per_page: query.perPage },
        };
        return NextResponse.json(body);
      }
    }
  });
}